Passed
Push — master ( 71c79b...ae58d1 )
by Barry
01:03
created

helpers.js ➔ cli   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 23
rs 8.5906
1
/*
2
=====fsnip======
3
4
fsnip is a command line utility to extract and modify json from a file.
5
6
*/
7
const fs = require('fs')
8
const jp = require('jsonpath')
9
const stringify = require('json-stringify-pretty-compact')
10
const chalk = require('chalk')
11
12
export function cli (args) {
13
  if (args.length === 3 && args[2] === '--help') {
14
    console.info('fsnip is a tool for extracting json snippets from json files.\n\n' +
15
                 'Usage:\n' + '' +
16
                 'TODO still \n' +
17
                 '  fsnip FILE [options [arguments]]    process the file and output the result to the console\n' +
18
                 '  FILE         Specifies the file to process.\n' +
19
                 '  --ellipsify  replaces the passed object with ellipses (...)\n' +
20
                 '                 but excludes any keys which follow prepended by ~\n' +
21
                 '                 eg. fsnip myfile.json --ellipsify $..address ~postcode')
22
  } else if (args.length >= 3) {
23
    try {
24
      var txt = fs.readFileSync(args[2]).toString()
25
    } catch (err) {
26
      console.error(chalk.redBright("unable to read file '" + args[2] + "'"))
27
    }
28
    if (typeof txt !== 'undefined') {
29
      console.info(fsnipDo(args.slice(3), txt))
30
    }
31
  } else { // we couldn't recognise the format on the command line
32
    console.error(chalk.redBright('Unrecognised arguments passed to fsnip. See fsnip --help'))
33
  }
34
}
35
36
function fsnipDo (cmdOpts, inputText) {
37
  // does the processing of the fsnip command
38
  // inputText is the text we want to modify
39
  if (cmdOpts === null || cmdOpts.length === 0) { return inputText } // no processing required as no options passed in
40
  var src = { // a temporary structure containing the text we are working on its type eg. 'json' (which is set later)
41
    text: inputText,
42
    type: '',
43
    outputOptions: {},
44
    error: [],
45
    json: null,
46
    plain: null
47
  }
48
49
  parseOptions()
50
  postProcess(src)
51
  return src.error.length === 0 ? src.text : chalk.redBright(src.error)
52
53
  function parseOptions () {
54
    // now we are going to parse through the options and arguments to extract individual options together with their arguments
55
    var cmdOpt = '' // current option from the cmdOptsString list
56
    var cmdArgs = [] // array containing any arguments for the cmdOpt
57
    for (var i = 0; i < cmdOpts.length; i++) {
58
      if (cmdOpts[i].substr(0, 2) === '--') { // this is a new option eg. --ellipsify
59
        processOption()
60
        cmdOpt = cmdOpts[i] // store the new option we have found
61
        cmdArgs = [] // reset ready for any new arguments
62
      } else {
63
        // this must be an argument for the current option
64
        if (cmdOpt === '') { // error if we don't currently have an option
65
          src.error.push("invalid argument '" + cmdOpts[i] + "' passed without valid option to fsnip")
66
        } else {
67
          cmdArgs.push(cmdOpts[i])
68
        }
69
      }
70
    }
71
    processOption()
72
73
    function processOption () {
74
      // process/run any option we've found
75
      if (cmdOpt !== '') {
76
        runOption(cmdOpt, cmdArgs, src)
77
      }
78
    }
79
  }
80
}
81
82
function runOption (option, args, inpObj) {
83
  // option is a string eg. '-jsonEllipsify'
84
  // arguments is an array of arguments for the option
85
  // inpObj is an object containing the text, type and json object we need to modify
86
  // this function acts as a marsheller to identify options and process them accordingly
87
  let funcs = {
88
    '--json': () => { json(inpObj) },
89
    '--prettify': () => { jsonPrettify(inpObj, args) },
90
    '--ellipsify': () => { jsonEllipsify(inpObj, args) },
91
    '--snip': () => { jsonSnippet(inpObj, args) },
92
    '--delKeys': () => { jsonDelKeys(inpObj, args) },
93
    '--from': () => { textFrom(inpObj, args, false) },
94
    '--start': () => { textFrom(inpObj, args, true) },
95
    '--to': () => { textTo(inpObj, args, false) },
96
    '--finish': () => { textTo(inpObj, args, true) }
97
  }
98
99
  if (funcs[option]) {
100
    funcs[option]()
101
  } else {
102
    inpObj.error.push("invalid option '" + option + "' for fsnip")
103
  }
104
}
105
106
function postProcess (inpObj) {
107
  // does any post process tidying up
108
  if (inpObj.type === 'json') {
109
    // stringify as required
110
    let opts = inpObj.outputOptions
111
    if (opts.maxLength === 'infinity' && opts.margins === false) {
112
      inpObj.text = JSON.stringify(inpObj.json)
113
    } else if (opts.maxLength === 0 && opts.margins === false) {
114
      inpObj.text = JSON.stringify(inpObj.json, null, opts.indent)
115
    } else {
116
      inpObj.text = stringify(inpObj.json, opts)
117
    }
118
    // now replace any placeholders. The placeholders are valid JSON but what we replace them with may not be valid JSON
119
    inpObj.text = inpObj.text.replace(/\[\s*"fsnipPlaceholderArrEllipses"\s*\]/g, '[...]')
120
    inpObj.text = inpObj.text.replace(/\{\s*"fsnipPlaceholderObj"\s*:\s*"Ellipses"\s*\}/g, '{...}') // do this separately to the one below so that if the object is empty it appears all on one line
121
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderObj"\s*:\s*"Ellipses"/g, '...')
122
    inpObj.text = inpObj.text.replace(/"fsnipPlaceholderStrEllipses"/g, '"..."')
123
  } else if (inpObj.type === 'plain') {
124
    inpObj.text = inpObj.plain.trim()
125
  }
126
}
127
128
export function setInputType (inpObj, newType) { // only exported for testing purposes
129
  if (typeof inpObj.type === 'undefined' || inpObj.type === '') { // type has not previously been set
130
    inpObj.type = newType
131
    if (newType === 'json') {
132
      inpObj.json = JSON.parse(inpObj.text)
133
      jsonPrettify(inpObj) // sets default output options for json
134
      return true
135
    } else if (newType === 'plain') {
136
      inpObj.plain = inpObj.text
137
      return true
138
    } else {
139
      return false
140
    }
141
  } else if (inpObj.type !== newType) { // it's already been set to something else so there's a problem
142
    if (typeof inpObj.error === 'undefined') { inpObj.error = [] }
143
    inpObj.error.push('cannot mix options designed to process different types of file')
144
    return false
145
  } else {
146
    return true
147
  }
148
}
149
150
function buildJsonSearchPath (keyName) {
151
  if (keyName.substr(0, 1) === '$') {
152
    return keyName
153
  } else {
154
    return "$..['" + keyName + "']"
155
  }
156
}
157
158
function removeQuotes (str) {
159
  // if the passed string has matching encapsulating quotes these are removed
160
  if ((str.substr(0, 1) === '\'' && str.substr(-1) === '\'') ||
161
      (str.substr(0, 1) === '"' && str.substr(-1) === '"')) {
162
    return str.substr(1, str.length - 2)
163
  } else {
164
    return str
165
  }
166
}
167
168
// =================json===============================
169
function json (inpObj) {
170
    // cmdArgs is an array of arguments
171
    // json is an object containing the json object we need to modify
172
  setInputType(inpObj, 'json') // all we do is flag our content as being json
173
}
174
175
// =================jsonPrettify=======================
176
function jsonPrettify (inpObj, cmdArgs) {
177
  // cmdArgs is an (optional) array of arguments being indent, maxLength, margins
178
  // they are all passed as strings so need to be converted to numbers where appropriate
179
  // we use - 0 to convert string numbers to numeric and === against itself to check for NaN
180
  if (setInputType(inpObj, 'json')) {
181
    let opts = inpObj.outputOptions
182
    // set defaults
183
    opts.margins = false
184
    opts.maxLength = 45
185
    opts.indent = 2
186
    // overwrite with any values passed in
187
    if (cmdArgs !== undefined) {
188
      if ((cmdArgs[0] - 0) === (cmdArgs[0] - 0)) { opts.indent = (cmdArgs[0] - 0) }
189
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) { opts.maxLength = (cmdArgs[1] - 0) }
190
      if (cmdArgs[1] === 'infinity') { opts.maxLength = 'infinity' }
191
      opts.margins = (cmdArgs[2] === 'true') // defaults to false if margins anything other than true
192
    }
193
  }
194
}
195
196
// =================ellipsify==========================
197
function jsonEllipsify (inpObj, cmdArgs) {
198
  // cmdArgs is an array of arguments
199
  // json is an object containing the json object we need to modify
200
201
  if (setInputType(inpObj, 'json')) {
202
    // we have two types of argument for Ellipsify, plain and exclude so separate them out
203
    var cmdArgsPlain = []
204
    var cmdArgsExclude = []
205
    for (let i = 0; i < cmdArgs.length; i++) {
206
      if (cmdArgs[i].substr(0, 1) === '~') {
207
        cmdArgsExclude.push(removeQuotes(cmdArgs[i].substr(1)))
208
      } else {
209
        cmdArgsPlain.push(removeQuotes(cmdArgs[i]))
210
      }
211
    }
212
    if (cmdArgsPlain.length === 0) { cmdArgsPlain.push('$') }
213
    for (let i = 0; i < cmdArgsPlain.length; i++) {
214
      minimizeJsonProperty(inpObj.json, cmdArgsPlain[i], cmdArgsExclude)
215
    }
216
  }
217
}
218
219
export function minimizeJsonProperty (json, property, excludes) { // only exported for test purposes
220
  // this function takes a json object as input.and for every occurrence of the given property puts a placeholder
221
  // but only if it is an array or an object.
222
  var arrPlaceholder = ['fsnipPlaceholderArrEllipses'] // a valid json array used as a placeholder to be replaced later with [...] (which is not valid json)
223
  var strPlaceholder = 'fsnipPlaceholderStrEllipses'
224
  var jsonPaths = jp.paths(json, buildJsonSearchPath(property)) // creates an array of all the paths of instances of the the property we want to minimize
225
  for (var i = 0; i < jsonPaths.length; i++) {
226
    let jsonPath = jp.stringify(jsonPaths[i])
227
    switch (jp.value(json, jsonPath).constructor.name) {
228
      case 'Object':
229
        delKeys(json, jsonPath, excludes)
230
        jp.value(json, jsonPath)['fsnipPlaceholderObj'] = 'Ellipses' // add a placeholder for the Ellipses
231
        break
232
      case 'Array':
233
        jp.value(json, jsonPath, arrPlaceholder)
234
        break
235
      case 'String':
236
        jp.value(json, jsonPath, strPlaceholder)
237
        break
238
      default:
239
        // do nothing
240
    }
241
  }
242
243
  function delKeys (json, jsonPath, excludes) {
244
    var keys = Object.keys(jp.value(json, jsonPath))
245
    for (var j = 0; j < keys.length; j++) {
246
      if (excludes.indexOf(keys[j]) === -1) {
247
        // this key is not in the excludes list so we need to delete it
248
        delete jp.value(json, jsonPath)[keys[j]]
249
      }
250
    }
251
  }
252
}
253
254
// ===================snip Function==============================
255
function jsonSnippet (inpObj, cmdArgs) {
256
  // cmdArgs is an array of arguments
257
  // inpObj is an object containing the json object we need to modify
258
  // the format of the call is eg.
259
  // '--snip vessel 2' which would extract the second instance of "vessel" in the json supplied
260
  // with the instance identifier being optional
261
  if (setInputType(inpObj, 'json')) {
262
    var occ = 1
263
    if (cmdArgs.length === 1) {
264
      occ = 1 // by default we snip the first occurrence of this property
265
    } else if (cmdArgs.length === 2) {
266
      if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
267
        occ = (cmdArgs[1] - 0)
268
        if (occ < 1) {
269
          inpObj.error.push('--snip requires its second argument to be a numeric values of at least 1 being the instance required')
270
          return
271
        }
272
      } else {
273
        inpObj.error.push("--snip requires its second argument to be numeric eg. '--snip vessel 2' with the optional second argument being the instance required")
274
        return
275
      }
276
    } else {
277
      inpObj.error.push("--snip requires 1 or 2 arguments eg. '--snip vessel 2' with the optional second argument being the instance required.")
278
      return
279
    }
280
    var jsonPaths = jp.paths(inpObj.json, buildJsonSearchPath(removeQuotes(cmdArgs[0]))) // creates an array of all the paths to this property
281
    if (jsonPaths.length < occ) {
282
      inpObj.error.push('--snip failed because there were only ' + jsonPaths.length + " occurrences of '" + removeQuotes(cmdArgs[0]) + "' found.")
283
      return
284
    }
285
    inpObj.json = jp.value(inpObj.json, jp.stringify(jsonPaths[occ - 1]))
286
  }
287
}
288
289
// ===================delKeys Function===========================
290
function jsonDelKeys (inpObj, cmdArgs) {
291
  // cmdArgs is an array of arguments
292
  // inpObj is an object containing the json object we need to remove keys from
293
  // the format of the call is eg.
294
  // '-jsonDelKeys vessel gnss' which would delete all instances of "vessel" and "gnss" in the json supplied
295
  if (setInputType(inpObj, 'json')) {
296
    for (var i = 0; i < cmdArgs.length; i++) {
297
      deleteJsonKey(inpObj.json, removeQuotes(cmdArgs[i]))
298
    }
299
  }
300
}
301
302
function deleteJsonKey (json, key) {
303
  // deletes all occurrences of key within json
304
  var jsonPaths = jp.paths(json, buildJsonSearchPath(key)) // creates an array of all the paths of instances of the key we want to delete
305
  var parent
306
  for (var i = 0; i < jsonPaths.length; i++) {
307
    let jsonPath = jp.stringify(jsonPaths[i])
308
    parent = jp.parent(json, jsonPath)
309
    if (Array.isArray(parent)) {
310
      parent.splice(jsonPaths[i][jsonPaths[i].length - 1], 1)
311
    } else {
312
      delete parent[jsonPaths[i][jsonPaths[i].length - 1]]
313
    }
314
  }
315
}
316
317
// ===================textFrom=================================
318
function textFrom (inpObj, cmdArgs, inclusive) {
319
  // cmdArgs is an array of arguments
320
  // inpObj is an object containing the text object we need to snip contents from
321
  // the format of the call is eg.
322
  // '--textFrom "some text" 2 - would start from the second instance of "some text"
323
  if (setInputType(inpObj, 'plain')) {
324
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--start' : '--from')
325
    if (x.found) {
326
      inpObj.plain = inpObj.plain.substr(x.loc + (inclusive === true ? 0 : x.len))
327
    }
328
  }
329
}
330
331
// ===================textTo===================================
332
function textTo (inpObj, cmdArgs, inclusive) {
333
  // cmdArgs is an array of arguments
334
  // inpObj is an object containing the text object we need to snip contents from
335
  // the format of the call is eg.
336
  // '--textTo "some text" 2 - would go up to the second instance of "some text"
337
  if (setInputType(inpObj, 'plain')) {
338
    let x = findLocation(inpObj, cmdArgs, inclusive ? '--finish' : '--to')
339
    if (x.found) {
340
      inpObj.plain = inpObj.plain.substring(0, x.loc + (inclusive === true ? x.len : 0))
341
    }
342
  }
343
}
344
345
function findLocation (inpObj, cmdArgs, errString) {
346
  // find the location of the nth occurrence of the text specified in the command arguments
347
  let occ
348
  if (cmdArgs.length === 1) {
349
    occ = 1 // by default we take from the first occurrence of this text
350
  } else if (cmdArgs.length === 2) {
351
    if ((cmdArgs[1] - 0) === (cmdArgs[1] - 0)) {
352
      occ = (cmdArgs[1] - 0)
353
      if (occ < 1) {
354
        inpObj.error.push(errString + ' requires its second argument to be a numeric value of at least 1 being the instance required')
355
        return {found: false}
356
      }
357
    } else {
358
      inpObj.error.push(errString + " requires its second argument to be numeric eg. '" + errString + " sometext 2' with the optional second argument being the instance required")
359
      return {found: false}
360
    }
361
  } else {
362
    inpObj.error.push(errString + " requires 1 or 2 arguments eg. '" + errString + " sometext' with the optional second argument being the instance required.")
363
    return {found: false}
364
  }
365
  let x = -1
366
  let arg = removeQuotes(cmdArgs[0])
367
  for (let i = 0; i < occ; i++) {
368
    x = inpObj.plain.indexOf(arg, x + 1)
369
  }
370
  if (x === -1) {
371
    inpObj.error.push('unable to find occurrence ' + occ + ' of "' + arg + '"')
372
  }
373
  return {found: (x !== -1), loc: x, len: arg.length}
374
}
375